input
中的 key
我们先来看一个切换登录方式的例子:
<div v-if="isUser">
<label>Login with account</label>
<input type="text" placeholder="Enter your account">
</div>
<div v-else>
<label>Login with email</label>
<input type="text" placeholder="Enter your email">
</div>
<button @click="isUser=!isUser">click to toggle</button>
我们会发现,在点击按钮切换登录方式后,输入框中已有的内容没有被清除,这是为什么呢?
引用官方文档的原话:
Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。
这里的 input
实际上复用了切换之前的 input
。而类似 <input>
,<select>
,<textarea>
这样的表单元素都有一个 internal state
保存着元素的值,在元素复用时,这个值是会得到保留的。
如果我们希望切换的时候不保留这个值呢?我们可以给两个 input
添加不同的 key
。因为 Vue 是将 key
作为唯一标识从而来识别复用的元素的,如果两个元素的 key
不同,那么就相当于告诉 Vue “这两个元素是完全独立的,你不能用其中一个来复用另一个”。
接着再来看一个利用 v-for
生成 input
的例子。
假如我们的代码为:
<div id="app">
<div v-for="(item,index) in array">
{{item}}: <input type="text">
</div>
</div>
const app = new Vue({
el:'#app',
data:{
array:["A","B","C","D","E"]
}
})
之后生成的 input
中我们填入字符串作为 internal state
。如图:
在没有使用 key
的情况下,我们通过 app.array.splice(2,0,"F")
在 BC 之间插入 F,发现:
和之前一样,因为 Vue 采用的是 就地复用
策略,这意味着 ABCDE 在原地不动的情况下被复用了,CDE 都被重新渲染了一次,但先前的 internal state
仍然保留着。
出于性能考虑,有没有办法可以只移动个别元素,单独渲染要插入的那个新元素呢?有了前面的经验,我们会想到给每个 input
一个 key
值。
首先我们尝试将 index
作为 key
,之后进行插入操作,发现:
问题依然存在。这是因为,我们将 index
作为复用的判断依据,相当于告诉 Vue:“只要这两个东西的 index 一样,就进行复用”。插入之前 C 的 index 是 2,插入之后 F 的 index 也是 2,于是 F 复用了 C,同理,DE 也被复用了,并因此重新渲染了一次。
index
是会随着插入删除改变的值,所以它实际上并不适合作为 key
。于是我们想:在进行插入或者删除操作的时候,有没有一种值始终不会改变呢?有的,我们可以给每个元素一个单独的 id。但更简单的方法是直接使用 item
,即元素本身的值,毕竟这个值对每个元素来说也是独一无二的。
我们将 item
作为 key
,之后进行插入操作,发现:
这回正常了。可以很明显地看到,每个元素都复用了先前的对应元素,这是因为此时 item
(即元素值)才是复用的判断依据,相当于告诉 Vue:“只要这两个东西的元素值一样,就进行复用”。例如对于 C 来说,它只会复用与自己的值一样的元素,显然这个元素就是 C 本身。同理,D 复用 D,E 复用 E,CDE 都不需要重新渲染了,只需要后移以方便 F 插入,这时候的性能显然要好很多。
Virtual DOM 的 Diff
算法
下面大致从虚拟DOM的Diff算法实现的角度去解释一下。
vue 和 react的虚拟 DOM 的 Diff
算法大致相同,其核心是基于两个简单的假设:
- 两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构。
- 同一层级的一组节点,他们可以通过唯一的id进行区分。基于以上这两点假设,使得虚拟DOM的Diff算法的复杂度从O(n^3)降到了O(n)。
引用 React’s diff algorithm 中的例子:
当某一层有很多相同的节点时,也就是列表节点时,Diff 算法的更新过程默认情况下也是遵循以上原则。 比如一下这个情况:
我们希望可以在 B 和 C 之间加一个 F,Diff 算法默认执行起来是这样的:
即把 C 更新成 F,D 更新成 C,E 更新成 D,最后再插入 E,这样显然很没有效率。
所以我们需要使用 key 来给每个节点做一个唯一标识,Diff 算法就可以正确的识别此节点,找到正确的位置区插入新的节点。
所以 key 的作用主要是为了高效的更新虚拟 DOM。
参考:
https://stackoverflow.com/questions/44077320/what-is-the-use-of-track-by-or-key-in-v-for-in-vue-js
https://juejin.im/post/5aae19aa6fb9a028d4445d1a#comment